//#include <bits/stl_alloc.h>
#include <stdexcept>
#include <string>
#include <algorithm>
#include <iostream>
#include <utility>
#include <vector>
#include <sstream>
#include <fstream>
#include <deque>
#include <boost/tuple/tuple.hpp>
#include <boost/regex.hpp>

//# define DEBUG_LEVEL 9
#include <debug_func.h>
#include <perlutil.h>
#include <ultoa.h>
#include <portable_timer.h>
#include <progress_indicator.h>
#include <bcpp_parsefasta.h>
#include <bcpp_read_seq.h>
#include <stlhelper.h>
#include <string_hash.h>
#include <print_error.h>
#include <encrypt/digest_n.h>
#include <encrypt/md5.h>


#include <cdna_prot.h>

#include "get_arg.h"

using BioCPP::t_collect_sequences;
using BioCPP::t_collect_sequences_report_errors_to_strm;
using std::cerr;
using std::cout;
using std::string;
using std::vector;
using std::deque;
using std::istream;
using std::ifstream;
using std::sort;
using std::ostringstream;
using boost::tie;


//________________________________________________________________________________________

//	t_scan_identifiers_to_hash

//________________________________________________________________________________________
using BioCPP::t_collect_sequences_report_errors;
struct t_scan_identifiers_to_hash
{
	std::string							seqid_regex_str;
	t_progress_indicator&				dots;
	t_collect_sequences_report_errors& 	error_report;
	unsigned							acc_beg_line;	// accession begin line for error
    cdna_prot::t_validate_prot			valid_seq_char;
    encryption::md5                     encrypt_func;

	// data to be retrieved
    t_index_string&					    identifiers;
    std::vector<std::string >&   	    hashes;
    t_scan_identifiers_to_hash(const std::string&				   seqid_regex_str_,
                               t_progress_indicator&			   dots_,
                               t_collect_sequences_report_errors&  error_report_,
                               t_index_string&					   identifiers_,
                               std::vector<string>&		           hashes_)
			:
			seqid_regex_str(seqid_regex_str_),
			dots(dots_),
			error_report(error_report_),
			acc_beg_line(0),
            identifiers(identifiers_),
            hashes(hashes_)		 {	}



	// callback
	bool operator()(std::string& accession, std::string& sequence,
						unsigned acc_filepos,
						unsigned seq_filepos);
};

bool t_scan_identifiers_to_hash::operator()(std::string& accession, std::string& sequence,
									unsigned /*acc_filepos*/, unsigned /*seq_filepos*/)
{
	// print progress dots
	++dots;

	// get line counts for error messages
	unsigned curr_acc_beg_line = acc_beg_line;
	acc_beg_line += std::count(sequence.begin(), sequence.end(), '\n') + 1;

	// bad sequence!!!
	if (!sequence.length())
	{
		error_report.zero_length_sequence(curr_acc_beg_line);
		return true;
	}
	if (!accession.length())
	{
		error_report.zero_length_accession(curr_acc_beg_line);
		return true;
	}




	// match regex

    static boost::regex seqid_regex(seqid_regex_str);
    boost::match_results<std::string::iterator> id_match;
    if (!boost::regex_search(accession.begin(), accession.end(), id_match, seqid_regex) ||
        !id_match[1].length())
    {
        error_report.no_matching_seq_id(curr_acc_beg_line, seqid_regex_str);
        return true;
    }

    string seq_id = id_match[1];

    // remove invalid characters
    string valid_seq = sequence;
    valid_seq_char.validate(valid_seq);

    t_digest_N<4> digest;
    encrypt_func(valid_seq, digest);
    string digest_string = digest.str();

    // new identifier?
    bool new_seqid_added = false;
    unsigned index = identifiers.index(seq_id, new_seqid_added);
    if (!new_seqid_added)
    {
        error_report.duplicated_seq_id(index, curr_acc_beg_line);
        if (hashes[index] != digest_string)
            throw std::runtime_error("Two different sequences with "
                                     "identical identifiers [" + seq_id + "]");
    }
    else
        hashes.push_back(digest_string);

    cout << ">" << digest_string << " " << accession << "\n";
    cout << sequence;
	return true;
}


//________________________________________________________________________________________

//	t_apply_hash

//________________________________________________________________________________________
struct t_apply_hashes_errors :
		public BioCPP::t_collect_sequences_collect_errors
{
	void unhashed_sequence(unsigned line) { unhashed_sequence_line_num.push_back(line);	}
	std::vector<unsigned> unhashed_sequence_line_num;
	virtual void report(std::ostream& os, bool detail) const;
};

void t_apply_hashes_errors::report(std::ostream& os, bool detail) const
{
	if (unhashed_sequence_line_num.size())
	{
		std_print_error(os, ultoa(unhashed_sequence_line_num.size()) +
							"\t unhashed sequences were found (without matching "
							"identifiers in the main fasta file)");
		if (detail)
		{
			os << "\ton the following lines:\n";
			BOOST_FOREACH(unsigned i, unhashed_sequence_line_num) 
				os << "\t" << i << "\n";
		}
	}
	BioCPP::t_collect_sequences_collect_errors::report(os, detail);
}


struct t_apply_hash
{
	std::string							seqid_regex_str;
	t_progress_indicator&				dots;
	t_apply_hashes_errors&			 	error_report;
	unsigned							acc_beg_line;	// accession begin line for error

    t_index_string&					    identifiers;
    std::vector<std::string >&   	    hashes;
	bool								ignore_unhashed;
	std::ostream&						ostrm;
    t_apply_hash        (const std::string&				    seqid_regex_str_,
                         t_progress_indicator&			    dots_,
                         t_apply_hashes_errors& 			error_report_,
                         t_index_string&				    identifiers_,
                         std::vector<string>&		        hashes_,
						 bool								ignore_unhashed_,
						 std::ostream&						ostrm_
						 )
			:
			seqid_regex_str(seqid_regex_str_),
			dots(dots_),
			error_report(error_report_),
			acc_beg_line(0),
            identifiers(identifiers_),
            hashes(hashes_),
			ignore_unhashed(ignore_unhashed_),
			ostrm(ostrm_){	}



	// callback
	bool operator()(std::string& accession, std::string& sequence,
						unsigned acc_filepos,
						unsigned seq_filepos);
};



bool t_apply_hash::operator()(std::string& accession, std::string& sequence,
							unsigned /*acc_filepos*/, unsigned /*seq_filepos*/)
{
	// print progress dots
	++dots;

	// get line counts for error messages
	unsigned curr_acc_beg_line = acc_beg_line;
	acc_beg_line += std::count(sequence.begin(), sequence.end(), '\n') + 1;

	// bad sequence!!!
	if (!sequence.length())
	{
		error_report.zero_length_sequence(curr_acc_beg_line);
		return true;
	}
	if (!accession.length())
	{
		error_report.zero_length_accession(curr_acc_beg_line);
		return true;
	}




	// match regex

    static boost::regex seqid_regex(seqid_regex_str);
    boost::match_results<std::string::iterator> id_match;
    if (!boost::regex_search(accession.begin(), accession.end(), id_match, seqid_regex) ||
        !id_match[1].length())
    {
        error_report.no_matching_seq_id(curr_acc_beg_line, seqid_regex_str);
        return true;
    }

    string seq_id = id_match[1];


    // new identifier?
    unsigned index;
    bool seq_id_hashed = identifiers.is_indexed(seq_id, index);
	if (!seq_id_hashed)
	{
		if (ignore_unhashed)
		{
			error_report.unhashed_sequence(curr_acc_beg_line);
			return true;
		}
		throw std::runtime_error("Sequence in the companion file with an unknown (unhashed) identifier [" + seq_id + "]");
	}

    ostrm << ">" << hashes[index] << " " << accession << "\n";
    ostrm << sequence;
	return true;
}


//________________________________________________________________________________________

//	hash_sequences

//________________________________________________________________________________________
void hash_sequences(	std::istream&					istrm,
							std::string						regex_str,
							t_progress_indicator&			dots,
							t_collect_sequences_report_errors& errors,
                            t_index_string&					identifiers,
                            std::vector<string>&         	hashes)
{
	boost::regex brackets("\\(.*\\)");
	if (!boost::regex_search(regex_str, brackets))
		regex_str = "(" + regex_str + ")";

	t_scan_identifiers_to_hash collect_hash
									( regex_str, dots, errors, identifiers, hashes);
	BioCPP::Process_FA_Sequences_tellg(istrm, collect_hash);
}


//________________________________________________________________________________________

//	apply_hashes_to_sequences

//________________________________________________________________________________________
void apply_hashes_to_sequences(	std::istream&					istrm,
								std::ostream&					ostrm,
								bool							ignore_unhashed,
							    std::string						regex_str,
							    t_progress_indicator&			dots,
							    t_apply_hashes_errors& 			errors,
                                t_index_string&					identifiers,
                                std::vector<string>&         	hashes)
{
	boost::regex brackets("\\(.*\\)");
	if (!boost::regex_search(regex_str, brackets))
		regex_str = "(" + regex_str + ")";

	t_apply_hash do_hash( regex_str, dots, errors, identifiers, hashes, ignore_unhashed, ostrm);
	BioCPP::Process_FA_Sequences_tellg(istrm, do_hash);
}






int main(int argc, char* argv[])
{
	debug_func_cerr( 8 );

	t_program_args args;

	try
	{

    	// don't use C streams
    	std::ios::sync_with_stdio(false);
    
    	// get command line arguments
    	if (!process_command_line_options(argc, argv, args))
    		return 1;
    
    
    	//
    	// read identifiers
    	//
    	t_index_string	            identifiers;
        std::vector<string>	        hashes;
    

        {
        	BioCPP::t_collect_sequences_collect_errors collect_errors;
        	{
        		args.VERBOSE() << "\tReading FASTA sequences from STDIN...\n";
        		t_progress_indicator dots(args.VERBOSE(), 10000);
        		hash_sequences(std::cin, args.regex, dots, collect_errors, identifiers, hashes);
        	}
    
            // print out tab delimited
            if (args.file_map.length())
            {
                args.VERBOSE() << "\tPrint out identifier map...\n";
                std::ofstream ofs (args.file_map.c_str());
                for (unsigned i = 0; i < hashes.size(); ++i)
                    ofs << identifiers[i] << "\t" << hashes[i] << "\n";
            }
            args.VERBOSE() << "\t" << identifiers.size() << " identifiers read.\n";
            collect_errors.report(args.VERBOSE(), false);
            collect_errors.report(args.CERR(), args.detail_errors);
        }


        // apply the hashes to another fasta file 
        if (args.file_companion.length())
        {
			std::ifstream ifs (args.file_companion.c_str());
			std::ofstream ofs (args.file_companion_output.c_str());
            t_apply_hashes_errors collect_errors;
            {
                args.VERBOSE() << "\tApplying hashes to "
                                  "sequences from " << args.file_companion << "...\n";
                t_progress_indicator dots(args.VERBOSE(), 10000);
                apply_hashes_to_sequences(ifs, ofs, args.ignore_unhashed, args.regex, dots, collect_errors, identifiers, hashes);
            }
    
            args.VERBOSE() << "\t" << identifiers.size() << " identifiers read.\n";
            collect_errors.report(args.VERBOSE(), false);
            collect_errors.report(args.CERR(), args.detail_errors);
        }


    	args.VERBOSE() << "\tDone...\n";


	}
	catch (std::exception& e)
	{
		std_print_error(args.CERR(), e.what());
		return 1;
	}
	catch(...)
	{
		return 1;
	}

	return 0;
}
